-- -------------------------------------------------------------------------
-- Features/PlayerInfo.lua
-- Player Information Overlay Panel
-- -------------------------------------------------------------------------

local module = {}

-- [Lua 5.4] <const> for function references
local math_floor <const> = math.floor
local math_sqrt <const> = math.sqrt
local math_max <const> = math.max
local string_format <const> = string.format
local string_len <const> = string.len
local tostring <const> = tostring
local pcall <const> = pcall
local os_clock <const> = os.clock
local ipairs <const> = ipairs

-- [Dependencies]
local dep = {
    ui = nil,
    config = nil,
    natives = nil,
    player_list = nil,
    renderer = nil
}

local N = {}

local global_T = nil
local function _T(text)
    if global_T then return global_T(text) end
    if _G._T then global_T = _G._T; return global_T(text) end
    return text
end

-- [Configuration]
local state = {
    scale = 1.0,
    last_scale_check = 0,
    colors = {},
    colors_valid = false,
    text_cache = {},
    text_cache_count = 0,
}

local Config = {
    Layout = {
        BaseWidth = 380,
        BaseGap = 15,
        PaddingX = 10,
        TextScale = 14,
        Rounding = 4,
        DefaultOverlayFontSize = 14,
        DefaultOverlayRadius = 4,
        SelectBoxHeight = 60
    },
    HealthOffset = 100,
    OverlayRowCount = 13,
    ScaleCheckInterval = 1.0,
    TextCacheMaxSize = 128
}

local DIRECTIONS <const> = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"}

-- [Cached Data]
local cached_stats = {
    hp = "", armor = "", ammo = "", speed = "", heading = "", wanted = ""
}
local cached_layout = {
    x = 0, y = 0, w = 0, scale = 1, font_base = 14, font_s = 14,
    radius = 4, pad = 10, row_h = 24, cy = 0, col_w = 0,
    c_bg = nil, c_val = nil, c_div = nil, c_warn = nil
}

local STAT_UPDATE_INTERVAL <const> = 0.05
local last_stat_update = 0
local cached_stat_data = {}

-- [Helper Functions]
local Utils = {}

function Utils.update_scale()
    local now = os_clock()
    if now - state.last_scale_check < Config.ScaleCheckInterval then return end
    state.last_scale_check = now

    if game and game.resolution then
        local res = game.resolution()
        if res and res.y then
            state.scale = math_max(0.5, res.y / 1080.0)
        end
    end
end

local function get_cached_color(name)
    if state.colors_valid and state.colors[name] then return state.colors[name] end

    local c_data
    if dep.config and dep.config.data and dep.config.data.colors then
        c_data = dep.config.data.colors[name]
    end

    local final_col
    if c_data then
        final_col = color(c_data.r, c_data.g, c_data.b, c_data.a)
    else
        if name == "Overlay Background" then final_col = color(20, 24, 30, 240)
        elseif name == "Overlay Text" then final_col = color(255, 255, 255, 255)
        elseif name == "Overlay Divider" then final_col = color(100, 150, 255, 255)
        elseif name == "Warning" then final_col = color(200, 200, 200, 150)
        elseif name == "SelectBg" then final_col = color(20, 24, 30, 200)
        else final_col = color(255, 255, 255, 255) end
    end

    state.colors[name] = final_col
    return final_col
end

function Utils.invalidate_color_cache()
    state.colors = {}
    state.colors_valid = false
end

local money_cache = {}
local MONEY_CACHE_MAX_SIZE <const> = 50

local function fmt_money(v)
    if not v then return "$0" end

    if money_cache[v] then
        return money_cache[v]
    end

    local result
    if v < 1000 then
        result = "$" .. tostring(math_floor(v))
    else
        local s = string_format("%d", math_floor(v))
        local len = string_len(s)
        local parts = {}
        local count = 0

        for i = len, 1, -1 do
            count = count + 1
            parts[#parts + 1] = string.sub(s, i, i)
            if count == 3 and i > 1 then
                parts[#parts + 1] = ","
                count = 0
            end
        end

        local reversed = {}
        for i = #parts, 1, -1 do
            reversed[#reversed + 1] = parts[i]
        end

        result = "$" .. table.concat(reversed)
    end

    if #money_cache > MONEY_CACHE_MAX_SIZE then
        money_cache = {}
    end
    money_cache[v] = result

    return result
end

local ip_cache = {}
local IP_CACHE_MAX_SIZE <const> = 30

local function fmt_ip(ip)
    if not ip or ip == 0 then return _T("N/A") end

    if ip_cache[ip] then
        return ip_cache[ip]
    end

    local n = math_floor(ip)
    if n < 0 then n = n + 4294967296 end

    local result = string_format("%d.%d.%d.%d",
        math_floor(n / 16777216) % 256,
        math_floor(n / 65536) % 256,
        math_floor(n / 256) % 256,
        n % 256)

    if #ip_cache > IP_CACHE_MAX_SIZE then
        ip_cache = {}
    end
    ip_cache[ip] = result

    return result
end

local function fmt_hash(h)
    return (not h or h == 0) and _T("N/A") or string_format("0x%X", math_floor(h))
end

local veh_name_cache = {}
local VEH_CACHE_MAX_SIZE <const> = 100

local function get_vehicle_name(model_hash)
    if not model_hash or model_hash == 0 then return _T("N/A") end

    if veh_name_cache[model_hash] then
        return veh_name_cache[model_hash]
    end

    local name = _T("N/A")

    if N.GET_DISPLAY_NAME_FROM_VEHICLE_MODEL then
        local label = N.GET_DISPLAY_NAME_FROM_VEHICLE_MODEL(model_hash)

        if label and label ~= "" and label ~= "CARNOTFOUND" then
            if N.GET_LABEL_TEXT then
                local ok, result = pcall(N.GET_LABEL_TEXT, label)
                if ok and result and result ~= "" then
                    name = result
                else
                    name = label
                end
            else
                name = label
            end
        end
    end

    if name == _T("N/A") then
        name = fmt_hash(model_hash)
    end

    if #veh_name_cache > VEH_CACHE_MAX_SIZE then
        veh_name_cache = {}
    end
    veh_name_cache[model_hash] = name

    return name
end

local function get_text_width(str, scale)
    if not str then return 0 end
    local key = str .. scale
    if state.text_cache[key] then return state.text_cache[key] end

    local w = 0
    if gui and gui.text_size then
        local s = gui.text_size(str, scale, {wrap = 0})
        if s then w = s.x end
    else
        w = #tostring(str) * (scale * 6)
    end

    if state.text_cache_count > Config.TextCacheMaxSize then
        state.text_cache = {}
        state.text_cache_count = 0
    end
    state.text_cache[key] = w
    state.text_cache_count = state.text_cache_count + 1
    return w
end

-- [Logic Functions]
local Logic = {}

function Logic.update_stats(player, ped)
    local now = os_clock()
    local s = cached_stats

    if now - last_stat_update < STAT_UPDATE_INTERVAL and cached_stat_data[player.id] then
        local c = cached_stat_data[player.id]
        s.hp = c.hp
        s.armor = c.armor
        s.ammo = c.ammo
        s.speed = c.speed
        s.heading = c.heading
        s.wanted = c.wanted
        return
    end

    s.hp, s.armor, s.ammo = _T("N/A"), _T("N/A"), _T("N/A")
    s.speed, s.heading, s.wanted = _T("N/A"), _T("N/A"), "0/5"

    local is_streamed = (ped and ped ~= 0)

    if player.heading then
        local h = player.heading
        local idx = math_floor(((h + 22.5) % 360) / 45) + 1
        s.heading = string_format("%s (%.0f)", _T(DIRECTIONS[idx]), h)
    elseif is_streamed and N.ENTITY_HEADING then
        local ok, h = pcall(N.ENTITY_HEADING, ped)
        if ok and h then s.heading = string_format("%.0f", h) end
    end

    if is_streamed and N.ENTITY_SPEED then
        local ok, spd = pcall(N.ENTITY_SPEED, ped)
        if ok and spd then s.speed = string_format("%.0f %s", spd * 3.6, _T("KPH")) end
    end

    if is_streamed and N then
        if N.GET_ENTITY_HEALTH and N.GET_ENTITY_MAX_HEALTH then
            local hp = N.GET_ENTITY_HEALTH(ped)
            local max = N.GET_ENTITY_MAX_HEALTH(ped)
            if hp and max then
                local off = Config.HealthOffset
                s.hp = (hp > off and hp - off >= max - off) and _T("Full") or tostring(math_max(0, hp - off))
            end
        end
        if N.GET_PED_ARMOUR then
            s.armor = tostring(N.GET_PED_ARMOUR(ped))
        end
        if N.GET_AMMO_IN_PED_WEAPON and player.weapon_model then
            local ammo = N.GET_AMMO_IN_PED_WEAPON(ped, player.weapon_model)
            if ammo and ammo >= 0 then s.ammo = tostring(ammo) end
        end
    end

    if N and N.GET_PLAYER_WANTED_LEVEL then
        local w = N.GET_PLAYER_WANTED_LEVEL(player.id)
        if w then s.wanted = w .. "/5" end
    end

    last_stat_update = now
    cached_stat_data[player.id] = {
        hp = s.hp,
        armor = s.armor,
        ammo = s.ammo,
        speed = s.speed,
        heading = s.heading,
        wanted = s.wanted
    }
end

-- [Layout Functions]
local function layout_reset(x, y, w, scale, f_size, rad)
    local L = cached_layout
    L.x, L.y, L.w, L.scale = x, y, w, scale

    L.font_base = f_size
    L.font_s = f_size * scale
    L.radius = rad * scale
    L.pad = Config.Layout.PaddingX * scale
    L.row_h = (f_size + 10) * scale
    L.cy = y + L.pad
    L.col_w = w * 0.5

    L.c_bg   = get_cached_color("Overlay Background")
    L.c_val  = get_cached_color("Overlay Text")
    L.c_div  = get_cached_color("Overlay Divider")
    L.c_warn = get_cached_color("Warning")
end

local function layout_draw_bg(rows)
    local L = cached_layout
    local h = (rows * L.row_h) + L.pad

    if dep.renderer then
        dep.renderer.draw_rect(L.x, L.y, L.w, h, "Overlay Background", L.radius)
    elseif gui and gui.rect then
        gui.rect(vec2(L.x, L.y), vec2(L.w, h))
           :filled():color(L.c_bg):rounding(L.radius):draw()
    end
end

local function layout_row(l1, v1, l2, v2, v1_ok, v2_ok)
    local L = cached_layout
    local y = L.cy
    local fs = L.font_s
    local fb = L.font_base

    if dep.renderer then
        dep.renderer.draw_text(l1, L.x + L.pad, y, fb, "Overlay Text")

        local w1 = dep.renderer.get_text_width(v1, fb)
        dep.renderer.draw_text(v1, L.x + L.col_w - w1 - L.pad, y, fb,
            (v1_ok == false) and "Warning" or "Overlay Text")

        dep.renderer.draw_text("|", L.x + L.col_w - 2, y - 2, fb, "Overlay Divider")

        if l2 then
            dep.renderer.draw_text(l2, L.x + L.col_w + L.pad, y, fb, "Overlay Text")
            local w2 = dep.renderer.get_text_width(v2, fb)
            dep.renderer.draw_text(v2, L.x + L.w - w2 - L.pad, y, fb,
                (v2_ok == false) and "Warning" or "Overlay Text")
        end
    else
        if not gui or not gui.text then
            L.cy = y + L.row_h
            return
        end

        gui.text(l1):position(vec2(L.x + L.pad, y)):color(L.c_val):scale(fs):draw()

        local w1 = get_text_width(v1, fs)
        local c1 = (v1_ok == false) and L.c_warn or L.c_val
        gui.text(v1):position(vec2(L.x + L.col_w - w1 - L.pad, y)):color(c1):scale(fs):draw()

        gui.text("|"):position(vec2(L.x + L.col_w - 2, y - 2)):color(L.c_div):scale(fs):draw()

        if l2 then
            gui.text(l2):position(vec2(L.x + L.col_w + L.pad, y)):color(L.c_val):scale(fs):draw()
            local w2 = get_text_width(v2, fs)
            local c2 = (v2_ok == false) and L.c_warn or L.c_val
            gui.text(v2):position(vec2(L.x + L.w - w2 - L.pad, y)):color(c2):scale(fs):draw()
        end
    end

    L.cy = y + L.row_h
end

-- [Module API]
function module.init(ui_lib, cfg_lib, nat_lib, _)
    dep.ui = ui_lib
    dep.config = cfg_lib
    dep.natives = nat_lib
    global_T = (_G and _G._T) or global_T

    local ok, renderer_module = pcall(require, "ClickUl.UI.renderer")
    if ok then
        dep.renderer = renderer_module
    end

    if nat_lib then
        local function get(name) return nat_lib[name] end

        N.GET_ENTITY_HEALTH = get("GET_ENTITY_HEALTH")
        N.GET_ENTITY_MAX_HEALTH = get("GET_ENTITY_MAX_HEALTH")
        N.GET_PED_ARMOUR = get("GET_PED_ARMOUR")
        N.GET_AMMO_IN_PED_WEAPON = get("GET_AMMO_IN_PED_WEAPON")
        N.GET_PLAYER_WANTED_LEVEL = get("GET_PLAYER_WANTED_LEVEL")
        N.GET_PLAYER_INVINCIBLE = get("GET_PLAYER_INVINCIBLE")
        N.ENTITY_SPEED = get("ENTITY_SPEED")
        N.ENTITY_HEADING = get("ENTITY_HEADING")

        N.GET_DISPLAY_NAME_FROM_VEHICLE_MODEL = get("GET_DISPLAY_NAME_FROM_VEHICLE_MODEL")
        N.GET_LABEL_TEXT = get("GET_LABEL_TEXT")
        N.GET_PLAYER_PED = get("GET_PLAYER_PED")
    end

    return true
end

function module.set_player_list(inst)
    dep.player_list = inst
end

local label_width_precached = false
local cached_scale = 0

local function precache_label_widths(font_size)
    if label_width_precached and cached_scale == font_size then return end

    local FIXED_LABELS = {
        _T("Name"), _T("RID"), _T("Rank"), _T("Role"),
        _T("Health"), _T("Armor"), _T("Input"), _T("Distance"),
        _T("IP"), _T("Token"), _T("Bank"), _T("Wallet"),
        _T("Wanted"), _T("Ammo"), _T("Model"), _T("Weapon"),
        _T("Status"), _T("Interior"), _T("Modder"), _T("GodMode"),
        _T("In Vehicle"), _T("Vehicle"), _T("K/D"), _T("Pos"),
        _T("Heading"), _T("Speed"), _T("Bounty")
    }

    for _, label in ipairs(FIXED_LABELS) do
        get_text_width(label, font_size)
    end

    label_width_precached = true
    cached_scale = font_size
end

function module.draw_overlay()
    if not dep.ui or not dep.ui.is_open() then return end
    if not dep.player_list then return end

    Utils.update_scale()
    local scale = state.scale
    local L_cfg = Config.Layout

    local o_width = math_floor(L_cfg.BaseWidth * scale)
    local o_gap = math_floor(L_cfg.BaseGap * scale)

    local cfg_data = dep.config.data
    local base = cfg_data.base
    local current = cfg_data.current
    local offset_x = base.overlay_offset_x or 10
    local ox = base.x + (current.w or (760 * scale)) + o_gap + math_floor(offset_x * scale)
    local oy = base.y

    local sel_id = dep.player_list.get_selected_player_id()

    local font_sz = base.overlay_font_size or L_cfg.DefaultOverlayFontSize
    local radius = base.overlay_radius or L_cfg.DefaultOverlayRadius

    precache_label_widths(font_sz * scale)

    state.colors_valid = true

    layout_reset(ox, oy, o_width, scale, font_sz, radius)

    if not sel_id then
        return
    end

    local p = players.get(sel_id)
    if not p then
        return
    end

    local ped = p.ped
    if (not ped or ped == 0) and N.GET_PLAYER_PED then
        ped = N.GET_PLAYER_PED(sel_id)
    end
    local is_streamed = (ped and ped ~= 0)

    Logic.update_stats(p, ped)
    local s = cached_stats

    local status_txt = _T("Idle")
    if p.in_vehicle then status_txt = p.driver and _T("Driving") or _T("Passenger")
    elseif not p.alive then status_txt = _T("Dead")
    elseif p.shooting then status_txt = _T("Shooting")
    elseif p.aiming then status_txt = _T("Aiming") end

    local is_session_host = false
    if game and game.host then
        local host_player = game.host()
        if host_player and host_player.id then
            is_session_host = (sel_id == host_player.id)
        end
    end

    local is_script_host = false
    if dep.player_list and dep.player_list.get_host_ids then
        local script_host_id = dep.player_list.get_host_ids()
        if script_host_id and script_host_id >= 0 then
            is_script_host = (sel_id == script_host_id)
        end
    end

    local role_txt = _T("Player")
    if p.boss then role_txt = _T("Boss")
    elseif p.in_ceo then role_txt = _T("Associate")
    elseif is_script_host then role_txt = _T("SCRIPT HOST")
    elseif is_session_host then role_txt = _T("SESSION HOST") end

    local dist_txt, pos_txt = _T("N/A"), _T("N/A")
    local me = players.me()
    if p.coords and me and me.coords then
        local v1, v2 = me.coords, p.coords
        local dx, dy, dz = v1.x - v2.x, v1.y - v2.y, v1.z - v2.z
        local d = math_sqrt(dx*dx + dy*dy + dz*dz)
        dist_txt = string_format("%.1fm", d)
        pos_txt = string_format("%.0f, %.0f, %.0f", v2.x, v2.y, v2.z)
    end

    local is_god = false
    if is_streamed and N.GET_PLAYER_INVINCIBLE then
        local ok, result = pcall(N.GET_PLAYER_INVINCIBLE, sel_id)
        if ok and result then is_god = result end
    end
    local is_modder = p.modder or is_god
    local veh_name = p.in_vehicle and get_vehicle_name(p.vehicle_model) or _T("N/A")

    layout_draw_bg(Config.OverlayRowCount)

    layout_row(_T("Name"), p.name or _T("Unknown"), _T("RID"), tostring(p.rockstar_id or 0))
    layout_row(_T("Rank"), tostring(p.rank or 0), _T("Role"), role_txt)
    layout_row(_T("Health"), s.hp, _T("Armor"), s.armor, is_streamed, is_streamed)

    if p.active_bounty and p.bounty and p.bounty > 0 then
        layout_row(_T("Bounty"), fmt_money(p.bounty), _T("Input"), p.controller and _T("Controller") or _T("K/M"))
    else
        layout_row(_T("Input"), p.controller and _T("Controller") or _T("K/M"), _T("Distance"), dist_txt)
    end

    layout_row(_T("IP"), fmt_ip(p.ip_address), _T("Token"), tostring(p.host_token or "N/A"))
    layout_row(_T("Bank"), fmt_money(p.bank), _T("Wallet"), fmt_money(p.wallet or p.cash))
    layout_row(_T("Wanted"), s.wanted, _T("Ammo"), s.ammo, true, is_streamed)
    layout_row(_T("Model"), fmt_hash(p.model), _T("Weapon"), fmt_hash(p.weapon_model))
    layout_row(_T("Status"), status_txt, _T("Interior"), p.in_interior and _T("YES") or _T("NO"))
    layout_row(_T("Modder"), is_modder and _T("YES") or _T("NO"), _T("GodMode"), is_god and _T("YES") or _T("NO"), is_modder, is_god)
    layout_row(_T("In Vehicle"), p.in_vehicle and _T("YES") or _T("NO"), _T("Vehicle"), veh_name, p.in_vehicle, p.in_vehicle)
    layout_row(_T("K/D"), string_format("%.2f", p.kd or 0), _T("Pos"), pos_txt)
    layout_row(_T("Heading"), s.heading, _T("Speed"), s.speed, true, is_streamed)
end

module.invalidate_color_cache = Utils.invalidate_color_cache

return module
